/**********************************************/
/**				hd.c by doom119				  */
/**				   June, 2010				  */
/**********************************************/
#include <harddisk.h>
#include <io.h>
#include <system.h>

#define MAJOR_NR 3
#include <blk.h>

#define MAX_ERRORS 7
#define MAX_HD 1	// only support one hard disk

struct hd_info hd_info[] = {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};

struct hd_partition hd[5*MAX_HD] = {{0, 0},};

#define port_read(port, buffer, nr) \
	__asm__("cld; rep; insw\n\t" \
			::"d"(port), "D"(buffer), "c"(nr))

#define port_write(port, buffer, nr) \
	__asm__("cld; rep; outsw\n\t" \
			::"d"(port), "S"(buffer), "c"(nr))

extern void hd_interrupt(void);

static int reset;
static int recalibrate;

void unexpected_hd_interrupt(void)
{
	printk("un hd interrupt");
}


int sys_setup(void *BIOS)
{
	static int callable = 1;
	struct buffer_head *bh;
	struct partition *p;
	int i;
	
	//confine that this function only run once
	if(!callable)
		return -1;
	callable = 0;

	hd_info[0].cyl = *(unsigned short*)BIOS;
	hd_info[0].head = *(unsigned char*)(2+BIOS);
	hd_info[0].xt = *(unsigned short*)(3+BIOS);
	hd_info[0].wpcom = *(unsigned short*)(5+BIOS);
	hd_info[0].ecc = *(unsigned char*)(7+BIOS);
	hd_info[0].control = *(unsigned char*)(8+BIOS);
	hd_info[0].s_timeout = *(unsigned char*)(9+BIOS);
	hd_info[0].f_timeout = *(unsigned char*)(10+BIOS);
	hd_info[0].c_timeout = *(unsigned char*)(11+BIOS);
	hd_info[0].lzone = *(unsigned short*)(12+BIOS);
	hd_info[0].sect = *(unsigned char*)(14+BIOS);
	hd_info[0].reserved = *(unsigned char*)(15+BIOS);

	hd[0].start_sector = 0;
	hd[0].nr_sectors = hd_info[0].head * hd_info[0].sect * hd_info[0].cyl;

	if(!(bh = bread(0x300, 0)))
		panic("setup bread err");
	if(bh->data[510] != 0x55 ||
			bh->data[511] != 0xaa)
		panic("setup 0xaa55   ");

	p = 0x1be + (void*)bh->data;
	for(i = 1; i < 5; ++i)
	{
		hd[i].start_sector = p->start_sect;
		hd[i].nr_sectors = p->nr_sects;
	}

	brelease(bh);

	//to mount file system

	return 0;
}

/* Return: 0 is errro */
static int ready(void)
{
	int retry = 10000;
	while(--retry && (inb_p(HD_STATUS)&0xc0) != 0x40);
	return retry;
}

/* Return: 0 is ok, 1 is error*/
static int result(void)
{
	int i = inb_p(HD_STATUS);

	if((i & (BUSY_STAT | READY_STAT | WRERR_STAT |
					SEEK_STAT | ERR_STAT)) == (READY_STAT | SEEK_STAT))
		return 0;
	if(i & 1) i = inb(HD_ERROR);
	return i;
}

/* return: 0 is ok, 1 is timeout */
static int busy(void)
{
	unsigned int i;

	for(i = 0; i < 10000; ++i)
	{
		if(READY_STAT == (inb_p(HD_STATUS) & (BUSY_STAT|READY_STAT)))
			break;
	}
	i = inb(HD_STATUS);
	i &= BUSY_STAT | READY_STAT | SEEK_STAT;
	if(i == READY_STAT | SEEK_STAT)
		return 0;
	printk("hd busy timeout");
	return 1;
}

static void reset_controller(void)
{
	int i;

	outb(4, HD_CMD);
	for(i = 0; i < 100; ++i) nop();
	outb(hd_info[0].control & 0x0f, HD_CMD);
	if(busy())
		printk("hd reset busy  ");
	if((i = inb(HD_ERROR) != 1))
		printk("hd reset failed");
}

static void reset_hd(int nr)
{
	reset_controller();
//	hd_out(nr, hd_info[nr].sect, hd_info[nr].head-1,
//			hd_info[nr].cyl, WIN_SPECIFY, &recal_intr);
}

static void bad_rw_intr(void)
{
	if(++CURRENT_REQUEST->errors >= MAX_ERRORS)
		end_request(0);
	if(CURRENT_REQUEST->errors > MAX_ERRORS/2)
		reset = 1;
}

static void recal_intr(void)
{
	if(result())
		bad_rw_intr();
	do_hd_request();
}

static void read_intr(void)
{
	if(result())
	{
		bad_rw_intr();
		do_hd_request();
		return;
	}

	port_read(HD_DATA, CURRENT_REQUEST->buffer, 256);
	CURRENT_REQUEST->errors = 0;
	CURRENT_REQUEST->buffer += 512;
	CURRENT_REQUEST->start_sector++;

	if(--CURRENT_REQUEST->nr_sectors)
	{
		do_hd = &read_intr;
		return;
	}

	end_request(1);
	do_hd_request();
}

static void write_intr(void)
{
	if(result())
	{
		bad_rw_intr();
		do_hd_request();
		return;
	}

	if(--CURRENT_REQUEST->nr_sectors)
	{
		CURRENT_REQUEST->start_sector++;
		CURRENT_REQUEST->buffer += 512;
		do_hd = &write_intr;
		port_write(HD_DATA, CURRENT_REQUEST->buffer, 256);
		return;
	}

	end_request(1);
	do_hd_request();
}

static void hd_out(unsigned int drive, unsigned int nsect, 
		unsigned int sect, unsigned int head, unsigned int cyl,
		unsigned int cmd, void (*intr_addr)(void))
{
	register int port asm("dx");

	if(drive > 0 || head > 15)
		panic("hd_out drive   ");
	if(!ready())
		panic("hd_out ready   ");

	do_hd = intr_addr;
	outb_p(hd_info[drive].control, HD_CMD);
	outb_p(hd_info[drive].wpcom >> 2, HD_PRECOMP);
	outb_p(nsect, HD_NSECTOR);
	outb_p(sect, HD_SECTOR);
	outb_p(cyl, HD_LCYL);
	outb_p(cyl >> 8, HD_HCYL);
	outb_p((0xa0 | (drive << 4) | head), HD_CURRENT);
	outb_p(cmd, HD_COMMAND);
}

void do_hd_request(void)
{
	int dev,block, sector, cyl, head, nsect;
	int i, r;

repeat:
	if(!CURRENT_REQUEST)
		return;
	if(MAJOR(CURRENT_REQUEST->dev) != MAJOR_NR)
		panic("do_hd_request 1");
	if(CURRENT_REQUEST->bh)
	{
		if(!CURRENT_REQUEST->bh->lock)
			panic("do_hd_request 2");
	}

	dev = MINOR(CURRENT_REQUEST->dev);
	block = CURRENT_REQUEST->start_sector;
	if(dev >= 5 || block+2 > hd[dev].nr_sectors)
	{
		end_request(0);
		goto repeat;
	}

	block += hd[dev].start_sector;
	dev /= 5;

	__asm__("divl %4"
			:"=a"(block), "=d"(sector)
			:"a"(block), "d"(0), "r"((unsigned long)hd_info[dev].sect));
	__asm__("divl %4\n\t"
			:"=a"(cyl), "=d"(head)
			:"0"(block), "1"(0), "r"((unsigned long)hd_info[dev].head));

	sector++;
	nsect = CURRENT_REQUEST->nr_sectors;

	if(reset)
	{
		reset = 0;
		recalibrate = 1;
		reset_hd(CURRENT_DEV);
		return;
	}
	if(recalibrate)
	{
		recalibrate = 0;
		hd_out(dev, hd_info[CURRENT_DEV].sect, 0, 0, 0,
				WIN_RESTORE, &recal_intr);
		return;
	}

	if(CURRENT_REQUEST->cmd == WRITE)
	{
		hd_out(dev, nsect, sector, head, cyl, WIN_WRITE, &write_intr);
		for(i = 0; i < 3000 && !(r = inb_p(HD_STATUS)&DRQ_STAT); ++i);
		if(!r)
		{
			bad_rw_intr();
			goto repeat;
		}
		port_write(HD_DATA, CURRENT_REQUEST->buffer, 256);
	}
	else if(CURRENT_REQUEST->cmd == READ)
	{
		hd_out(dev, nsect, sector, head, cyl, WIN_READ, &read_intr);
	}
	else
		panic("do_hd_request 3");
}

void hd_init(void)
{
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
	set_interrupt_gate(0x2e, &hd_interrupt);
	outb_p(inb_p(0x21)&0xfb, 0x21);
	outb(inb_p(0xa1)&0xbf, 0xa1);

	printk("hd_init OK     ");
}
